/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.snail.main;

import com.snail.beans.CmdResult;
import com.snail.beans.RequestResult;
import com.snail.utils.HTMLDecoder;
import com.snail.cfg.XConfig;
import com.snail.cfg.XLogger;
import com.woniu.utils.datetime.DateTime;
import com.woniu.utils.io.file.TextWriter;
import com.woniu.utils.net.http.HttpGetForm;
import com.woniu.utils.net.http.HttpGetPostResponse;
import com.woniu.utils.net.http.HttpPostForm;
import com.woniu.utils.net.http.HttpToolkit;
import it.sauronsoftware.cron4j.Scheduler;
import it.sauronsoftware.cron4j.Task;
import it.sauronsoftware.cron4j.TaskExecutionContext;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.TimeZone;
import java.util.regex.Pattern;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.HtmlEmail;
import org.apache.commons.mail.SimpleEmail;
import org.apache.http.HttpResponse;
import org.jsoup.nodes.Element;

/**
 *
 * @author pm
 */
public abstract class XTask extends Task {

    private int error_count = 0, skipped_count = -1, warned_count = 0;
    protected Element full_cfg;
    protected Element cfg, module;
    protected Scheduler scheduler;
    protected String taskId;
    private boolean hasError = false;
    long hasErrorTime = 0;

    public XTask(Element full_cfg, Element cfg, Element module) {
        this.cfg = cfg;
        this.module = module;
        this.full_cfg = full_cfg;
    }

    public void setTaskId(String taskId, Scheduler scheduler) {
        this.taskId = taskId;
        this.scheduler = scheduler;
    }

    public void stop() {
        if (scheduler != null) {
            scheduler.deschedule(taskId);
        }
    }

    public abstract void onExecute(TaskExecutionContext tec);

    @Override
    public void execute(TaskExecutionContext tec) throws RuntimeException {
        try {
            onExecute(tec);
        } catch (Exception e) {
            XLogger.logger.error("thread execute  exception : ", e);
        }
    }

    public static CmdResult runCommand(Element command_cfg) {
        CmdResult result = new CmdResult();
        if (command_cfg == null) {
            return result;
        }
        Element cmd = command_cfg;
        if (!getText(cmd).isEmpty()) {
            int cmd_timeout = Integer.parseInt(cmd.attr("timeout"));
            result = exec(getText(cmd), cmd_timeout);
            if (!result.isSuccess()) {
                XLogger.logger.warn("exec fail : " + getText(cmd) + " " + result.getOutput());
            }
            return result;
        }
        return result;
    }

    protected static CmdResult exec(String command, int timeout) {
        CmdResult result = new CmdResult();
        //执行串行命令
        String randomName = System.getProperty("java.io.tmpdir") + "/commander_" + String.valueOf(Math.random()).substring(2) + ".sh";
        String content = "#!/bin/bash\n" + command;
        XLogger.logger.trace("try to create tmp file : " + randomName + " of command : " + command);
        TextWriter writer = new TextWriter("UTF-8").setOutputFile(randomName);
        if (writer != null) {
            writer.write(content);
            writer.close();
            XLogger.logger.trace("create tmp file success : " + randomName);
            File file;
            if ((file = new File(randomName)).isFile()) {
                //执行单个命令
                result = execCmd(("/bin/bash " + randomName), timeout);
                file.delete();
                XLogger.logger.trace("delete tmp file : " + randomName);
                return result;

            } else {
                XLogger.logger.error(" write file fail:" + "[" + randomName + "]", true);
            }
        } else {
            XLogger.logger.error(" write file fail:" + "[" + randomName + "]", true);
        }
        return result;
    }

    private static CmdResult execCmd(String commandstr, int timeout) {
        String[] commands = commandstr.split(" ");
        CmdResult result = new CmdResult();
        try {
            XLogger.logger.trace("exec : " + commandstr);
            Process p = Runtime.getRuntime().exec(commands);
            int execTimeout = timeout;
            TimeoutManager.addExec(p, execTimeout > 0 ? TimeoutManager.getTime() + execTimeout : 0);
            String output = "", error_output = "";
            try {
                output = getStreamOutput(p.getInputStream());
                error_output = getStreamOutput(p.getErrorStream());
                XLogger.logger.trace("exec done : " + commandstr);
            } catch (IOException e) {
                XLogger.logger.warn("exec io error : " + commandstr, e);
            }
            if (TimeoutManager.getExecTiemout(p) == -1) {
                XLogger.logger.warn(" exec timeout : " + commandstr);
                TimeoutManager.removeExecTiemout(p);
                return result;
            }
            TimeoutManager.removeExecTiemout(p);
            if (!error_output.isEmpty()) {
                result.setSuccess(false);
                result.setOutput(error_output);
                return result;
            } else {
                result.setSuccess(true);
                result.setOutput(output);
                return result;
            }
        } catch (IOException ex) {
            XLogger.logger.warn("exec " + commandstr, ex);
            result.setSuccess(false);
            result.setOutput(ex.getMessage());
            return result;
        }
    }

    private static String getStreamOutput(InputStream is) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String line, output;
        StringBuilder sbLines = new StringBuilder();
        while ((line = br.readLine()) != null) {
            sbLines.append(line).append("\n");
        }
        output = sbLines.toString();
        is.close();
        br.close();
        return output;
    }

    public static RequestResult httpRequest(Element req_cfg, String status_code, String output, long keepSeconds) {
        if (req_cfg == null) {
            return new RequestResult().setHasError(false);
        }
        String req_method = req_cfg.attr("method");
        String req_url = req_cfg.attr("url");

        if (req_url != null && !req_url.isEmpty()) {
            XLogger.logger.trace("request to :" + req_url);
            HttpGetPostResponse response;
            String code = req_cfg.attr("code");
            if ("get".equalsIgnoreCase(req_method)) {
                HttpGetForm form = new HttpGetForm();
                if (req_cfg.select(">form>item").size() > 0) {
                    for (Element item : req_cfg.select(">form>item")) {
                        form.add(item.attr("name"), getText(item));
                    }
                }
                if (XConfig.cfg().select(">global>request[method=get]>form>item").size() > 0) {
                    for (Element item : XConfig.cfg().select(">global>request[method=get]>form>item")) {
                        form.add(item.attr("name"), getText(item));
                    }
                }
                if (req_url.startsWith("https://")) {
                    response = https().doGet(req_url, form);
                } else {
                    response = http().doGet(req_url, form);
                }
            } else {
                HttpPostForm form = new HttpPostForm();
                if (req_cfg.select(">form>item").size() > 0) {
                    for (Element item : req_cfg.select(">form>item")) {
                        form.add(item.attr("name"), parseContent(getText(item), status_code, output, keepSeconds));
                    }
                }
                if (XConfig.cfg().select(">global>request[method=post]>form>item").size() > 0) {
                    for (Element item : XConfig.cfg().select(">global>request[method=post]>form>item")) {
                        form.add(item.attr("name"), getText(item));
                    }
                }
                if (req_url.startsWith("https://")) {
                    response = https().doPost(req_url, form);
                } else {
                    response = http().doPost(req_url, form);
                }
            }
            String errType = "";
            HttpResponse resp = response.getResponse();
            if (resp != null) {
                String resp_code = String.valueOf(resp.getStatusLine().getStatusCode());
                if (!resp_code.equals(code)) {
                    errType = "code";
                    XLogger.logger.warn("request error [" + resp_code + "] : " + req_url);

                } else if (req_cfg.select(">flag").size() > 0) {
                    String checkContent = response.getString();
                    if (!checkFlag(req_cfg.select(">flag").first(), checkContent)) {
                        errType = "content";
                        XLogger.logger.warn("request error [response content error] : " + req_url + " [response content] : " + checkContent);
                    }
                }
            } else {
                errType = "timeout";
                XLogger.logger.warn("request timeout  : " + req_url);
            }
            response.abort();
            RequestResult result = new RequestResult();
            if (!errType.isEmpty()) {
                result.setHasError(true);
                switch (errType) {
                    case "code":
                        result.setCodeError(true);
                        break;
                    case "content":
                        result.setContentError(true);
                        break;
                    case "timeout":
                        result.setTimeout(true);
                        break;
                }
            } else {
                result.setHasError(false);
            }
            return result;
        } else {
            return new RequestResult().setHasError(false);
        }
    }

    public static HttpToolkit http() {
        return http(0);
    }

    public static HttpToolkit http(int timeout) {
        if (timeout <= 0) {
            timeout = 3000;
        }
        HttpToolkit http = new HttpToolkit(HttpToolkit.getDefaultHttpClient());
        http.setRedirectable(false);
        http.getHttpClient().getParams().setParameter("http.socket.timeout", 3000);
        http.getHttpClient().getParams().setParameter("http.connection.timeout", timeout);
        return http;
    }

    public static HttpToolkit https() {
        return https(0);
    }

    public static HttpToolkit https(int timeout) {
        if (timeout <= 0) {
            timeout = 3000;
        }
        HttpToolkit http = new HttpToolkit(HttpToolkit.getDefaultHttpsClient());
        http.setRedirectable(false);
        http.getHttpClient().getParams().setParameter("http.socket.timeout", 3000);
        http.getHttpClient().getParams().setParameter("http.connection.timeout", timeout);
        return http;
    }

    public static boolean checkFlag(Element flag_cfg, String output) {
        if (output == null) {
            output = "";
        }
        if (flag_cfg == null) {
            return true;
        }
        String type = flag_cfg.attr("type");
        String value = getText(flag_cfg);
        XLogger.logger.trace("check flag[" + type + "] , content: " + output);
        try {
            boolean success = false;
            switch (type) {
                case "include":
                    success = output.contains(value);
                    break;
                case "exclude":
                    success = !output.contains(value);
                    break;
                case "empty":
                    success = output.isEmpty() || output.equals("\n");
                    break;
                case "any":
                    success = true;
                    break;
                case "http":
                    Element req_cfg = flag_cfg.select(">request").first();
                    if (req_cfg != null) {
                        RequestResult result = httpRequest(req_cfg, "", output, 0);
                        success = !result.isHasError();
                    } else {
                        success = true;
                    }
                    break;
                case "number":
                    success = Pattern.compile("^\\d+\\.?\\d*$", Pattern.MULTILINE | Pattern.DOTALL).matcher(output).find();
                    break;
                case "reg":
                    success = Pattern.compile(value, Pattern.MULTILINE | Pattern.DOTALL).matcher(output).find();
                    break;
                case ">=":
                    success = Long.parseLong(output.trim().replaceAll("[\\t\\s\\n]", "")) >= Long.parseLong(value);
                    break;
                case "=":
                    success = Long.parseLong(output.trim().replaceAll("[\\t\\s\\n]", "")) == Long.parseLong(value);
                    break;
                case "<=":
                    success = Long.parseLong(output.trim().replaceAll("[\\t\\s\\n]", "")) <= Long.parseLong(value);
                    break;
            }
            XLogger.logger.trace("check flag[" + type + "] : " + (success ? "success" : "fail"));
            return success;
        } catch (NumberFormatException e) {
            XLogger.logger.warn("parse flag error : ", e);
        }
        return false;
    }

    public void sendMail(Element mailto_cfg, String status_code, String output, long time) {
        if (mailto_cfg == null) {
            return;
        }
        if (mailto_cfg.select(">to").isEmpty() && full_cfg.select(">global>mailto>to").isEmpty()) {
            XLogger.logger.warn("send mail fail, to user is empty!");
            return;
        }
        String account_id = mailto_cfg.attr("account_id");
        if (account_id == null || account_id.isEmpty() || full_cfg.select(">email>account[id=" + account_id + "]").isEmpty()) {
            XLogger.logger.warn("account of id:" + account_id + " does not exists");
            return;
        }
        Element account_cfg = full_cfg.select(">email>account[id=" + account_id + "]").first();
        try {
            String charset;
            boolean isHtml = false;

            if (mailto_cfg.select(">isHtml").size() > 0) {
                isHtml = getText(mailto_cfg.select(">isHtml").first()).equalsIgnoreCase("true");
            } else if (full_cfg.select(">global>mailto>isHtml").size() > 0) {
                isHtml = getText(full_cfg.select(">global>mailto>isHtml").first()).equalsIgnoreCase("true");
            }

            if (mailto_cfg.select(">charset").size() > 0) {
                charset = getText(mailto_cfg.select(">charset").first()).toUpperCase();
            } else if (full_cfg.select(">global>mailto>charset").size() > 0) {
                charset = getText(full_cfg.select(">global>mailto>charset").first()).toUpperCase();
            } else {
                charset = "UTF-8";
            }

            Email mailer;
            if (isHtml) {
                mailer = new HtmlEmail();
            } else {
                mailer = new SimpleEmail();
            }
            String user = getText(account_cfg.select(">username").first());
            mailer.setCharset(charset);
            mailer.setHostName(getText(account_cfg.select(">host").first()));
            mailer.setSmtpPort(Integer.parseInt(getText(account_cfg.select(">port").first())));
            mailer.setSSLOnConnect(getText(account_cfg.select(">ssl").first()).equalsIgnoreCase("true"));
            mailer.setAuthentication(user, getText(account_cfg.select(">password").first()));
            mailer.setFrom(user);
            String title = parseContent(getText(mailto_cfg.select(">title").first()), status_code, output, time);
            String content = parseContent(getText(mailto_cfg.select(">content").first()), status_code, output, time);
            mailer.setSubject(title);
            mailer.setMsg(content);
            int size1 = mailto_cfg.select(">to").size();
            int size2 = full_cfg.select(">global>mailto>to").size();
            StringBuilder email_str = new StringBuilder();
            HashSet<String> tosSet = new HashSet<>();
            String email;
            for (int i = 0; i < size1; i++) {
                tosSet.add(email = getText(mailto_cfg.select(">to").get(i)));
                email_str.append(email + " ");
            }
            for (int i = 0; i < size2; i++) {
                tosSet.add(email = full_cfg.select(">global>mailto>to").get(i).text());
                email_str.append(email + " ");
            }
            String tos[] = new String[tosSet.size()];
            int i = 0;
            for (String v : tosSet) {
                tos[i++] = v;
            }
            mailer.addTo(tos);
            mailer.send();
            XLogger.logger.trace("send mail [" + title + "] to : " + email_str);
        } catch (EmailException ex) {
            XLogger.logger.warn("send email fail : ", ex);
        }
    }

    protected void onSuccess(String text) {
        if (hasError) {
            hasError = false;
            onRecovery(text);
        }
        error_count = 0;
        skipped_count = -1;
        warned_count = 0;
        Element success_cfg = cfg.select(">success").first();
        if (success_cfg != null) {
            runCommand(success_cfg.select(">cmd").first());
            httpRequest(success_cfg.select(">request").first(), null, text, 0);
            sendMail(success_cfg.select(">mailto").first(), null, text, 0);
        }

    }

    private void onRecovery(String text) {
        Element recovery_cfg = cfg.select(">recovery").first();
        if (recovery_cfg != null) {
            long error_seconds = getTimestamp() - hasErrorTime;
            runCommand(recovery_cfg.select(">cmd").first());
            httpRequest(recovery_cfg.select(">request").first(), null, text, error_seconds);
            sendMail(recovery_cfg.select(">mailto").first(), null, text, error_seconds);

        }
    }

    protected void onError(String type, String text) {
        error_count++;
        Element error_cfg = cfg.select(">error").first();
        if (error_cfg != null) {
            int warn_count = Integer.parseInt(error_cfg.attr("warn_count") != null && !error_cfg.attr("warn_count").isEmpty() ? error_cfg.attr("warn_count") : "1");
            int skip_count = Integer.parseInt(error_cfg.attr("skip_count") != null && !error_cfg.attr("skip_count").isEmpty() ? error_cfg.attr("skip_count") : "0");
            int max_warn_count = Integer.parseInt(error_cfg.attr("max_warn_count") != null && !error_cfg.attr("max_warn_count").isEmpty() ? error_cfg.attr("max_warn_count") : "0");
            if (error_count >= warn_count) {
                if (skip_count > 0) {
                    if (++skipped_count <= skip_count) {
                        if (skipped_count > 0) {
                            XLogger.logger.trace("skipped:" + String.valueOf(skipped_count));
                            return;
                        }
                    } else {
                        skipped_count = -1;
                    }
                }
                if (!hasError) {
                    hasError = true;
                    hasErrorTime = getTimestamp();
                }
                error_count = 0;
                if (max_warn_count > 0 && (++warned_count) > max_warn_count) {
                    XLogger.logger.trace("max warn count reached , warned:" + String.valueOf(warned_count) + "/max:" + String.valueOf(max_warn_count));
                    return;
                }
                runCommand(error_cfg.select(">cmd").first());
                httpRequest(error_cfg.select(">request").first(), type, text, 0);
                sendMail(error_cfg.select(">mailto").first(), type, text, 0);
            }
        }

    }

    private static String parseContent(String content, String status_code, String output, long keepSeconds) {
        status_code = status_code == null ? "" : status_code;
        output = output == null ? "" : output;
        return content.replaceAll("\\{code\\}", status_code)
                .replaceAll("\\{output\\}", output.replaceAll("\\$", "\\\\\\$"))
                .replaceAll("\\{err_time\\}", String.valueOf(keepSeconds))
                .replaceAll("\\{err_time_str\\}", parseErrorTime(keepSeconds))
                .replaceAll("\\{time\\}", String.valueOf(getTimestamp()))
                .replaceAll("\\{time_str\\}", DateTime.toString(getTimestamp()));
    }

    public static long getTimestamp() {
        TimeZone.setDefault(TimeZone.getTimeZone(XConfig.getTimezone()));
        return System.currentTimeMillis() / 1000;
    }

    /**
     * 解析秒为：x天x时x分x秒
     *
     * @param error_seconds
     * @return
     */
    private static String parseErrorTime(long error_seconds) {
        long info[] = new long[4];
        String subfix[] = new String[]{"天", "时", "分", "秒"};
        long days = info[0] = error_seconds / (24 * 3600);
        //System.out.println(days);
        long hours = info[1] = (error_seconds - days * 24 * 3600) / 3600;
        //System.out.println(hours);
        long minutes = info[2] = (error_seconds - (days * 24 * 3600 + hours * 3600)) / 60;
        //System.out.println(minutes);
        long seconds = info[3] = (error_seconds - (days * 24 * 3600 + hours * 3600 + minutes * 60));
        //System.out.println(seconds);
        StringBuilder error_time = new StringBuilder();
        int index = 3;
        for (int i = 0; i < info.length; i++) {
            long l = info[i];
            if (l != 0) {
                index = i;
                break;
            }
        }
        for (int i = 0; i < info.length; i++) {
            long l = info[i];
            if (i < index) {
                continue;
            }
            error_time.append(String.valueOf(l) + subfix[i]);
        }
        return error_time.toString();
    }

    protected static String getText(Element el) {
        if (el == null) {
            return "";
        }
        return HTMLDecoder.decode(el.html());
    }
}
